// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2020, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * Voltage regulation driver for LomboTech
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 */

#include <linux/slab.h>
#include <linux/regulator/consumer.h>
#include <linux/pm_domain.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/pm_clock.h>
#include <linux/device.h>

struct lombo_pm_domain {
	struct regulator **r;
	struct generic_pm_domain *p;
	int index;
	int dindex;
};

static struct lombo_pm_domain *lpd;

static int lombo_pd_power_off(struct generic_pm_domain *domain)
{
	int index;
	int ret;

	index = ((void *)domain - (void *)lpd->p) / sizeof(struct generic_pm_domain);
	ret = regulator_disable(lpd->r[index]);
	if (ret) {
		pr_err("power off pm domain: %s, failed\n", domain->name);
		return ret;
	}

	return 0;
}

static int lombo_pd_power_on(struct generic_pm_domain *domain)
{
	int index;
	int ret;

	index = ((void *)domain - (void *)lpd->p) / sizeof(struct generic_pm_domain);
	ret = regulator_enable(lpd->r[index]);
	if (ret) {
		pr_err("power on pm domain: %s, failed\n", domain->name);
		return ret;
	}

	return 0;
}

static int lombo_pd_attach_dev(struct generic_pm_domain *domain,
			  struct device *dev)
{
	int index;
	struct generic_pm_domain_data *gpd;

	gpd = dev_gpd_data(dev);
	index = ((void *)domain - (void *)lpd->p) / sizeof(struct generic_pm_domain);

	gpd->data = (void *)lpd->r[index];

	return 0;
}

static void lombo_pd_detach_dev(struct generic_pm_domain *domain,
			   struct device *dev)
{
	struct generic_pm_domain_data *gpd;

	gpd = dev_gpd_data(dev);

	gpd->data = NULL;
}

static int lombo_create_pm_node
(struct lombo_pm_domain *lpd, struct device_node *np)
{
	int ret;
	const char *string;

	ret = of_property_read_string_index(np,
		"pd-supply", 0, &string);
	if (ret < 0) {
		pr_err("failed to get domain suppler name %s: %d\n",
			__func__, __LINE__);
		return -EINVAL;
	}

	lpd->r[lpd->index] = regulator_get(NULL, string);
	if (lpd->r[lpd->index] == NULL) {
		pr_err("failed to get domain suppler %s: %d\n",
			__func__, __LINE__);
		return -EINVAL;
	}

	/* initialize the power-domain */
	lpd->p[lpd->index].name = np->name;
	lpd->p[lpd->index].power_off = lombo_pd_power_off;
	lpd->p[lpd->index].power_on = lombo_pd_power_on;
	lpd->p[lpd->index].attach_dev = lombo_pd_attach_dev;
	lpd->p[lpd->index].detach_dev = lombo_pd_detach_dev;
	lpd->p[lpd->index].flags = GENPD_FLAG_PM_CLK | GENPD_FLAG_ACTIVE_WAKEUP;

	ret = pm_genpd_init(&lpd->p[lpd->index], NULL, true);
	if (ret < 0) {
		pr_err("failed to initialize pm domain: %s, %s: %d\n",
			np->name, __func__, __LINE__);
		devm_regulator_put(lpd->r[lpd->index]);
		lpd->r[lpd->index] = NULL;
		return -EINVAL;
	}

	of_genpd_add_provider_simple(np, &lpd->p[lpd->index]);

	(lpd->index)++;

	return 0;
}

static int lombo_pm_add_domains
(struct lombo_pm_domain *lpd, struct device_node *parent)
{
	struct device_node *np;
	int cur_index, c_index;
	int ret = 0;

	cur_index = lpd->index;

	for_each_child_of_node(parent, np) {

		ret = lombo_create_pm_node(lpd, np);
		if (ret)
			return ret;

		c_index = lpd->index;

		if (of_get_child_count(np)) {
			ret = lombo_pm_add_domains(lpd, np);
			if (ret)
				return ret;
		} else {
			if (cur_index != 0) {
				ret = pm_genpd_add_subdomain(
					&lpd->p[cur_index - 1],
					&lpd->p[lpd->index - 1]);
				if (ret) {
					pr_err(
					"building connection between %s"
					" and %s failed\n",
					parent->name, np->name);
					return ret;
				}
			}
			continue;
		}

		if (cur_index != 0) {
			ret = pm_genpd_add_subdomain(
				&lpd->p[cur_index - 1],
				&lpd->p[c_index - 1]);
			if (ret) {
				pr_err(
				"building connection between"
				" %s and %s failed\n",
				parent->name, np->name);
				return ret;
			}
		}
	}

	return ret;
}

static void lombo_pm_remove_domains
(struct lombo_pm_domain *lpd, struct device_node *parent)
{
	struct device_node *np;
	int cur_index;

	for_each_child_of_node(parent, np) {

		++(lpd->dindex);
		cur_index = lpd->dindex;

		if (of_get_child_count(np))
			lombo_pm_remove_domains(lpd, np);
		else {
			if (lpd->r[lpd->dindex - 1] != NULL) {
				regulator_put(lpd->r[lpd->dindex - 1]);
				lpd->r[lpd->dindex - 1] = NULL;
				of_genpd_del_provider(np);
				pm_genpd_remove(&lpd->p[lpd->dindex - 1]);
			}
			continue;
		}

		if (lpd->r[cur_index - 1] != NULL) {
			regulator_put(lpd->r[cur_index - 1]);
			lpd->r[cur_index - 1] = NULL;
			of_genpd_del_provider(np);
			pm_genpd_remove(&lpd->p[cur_index - 1]);
		}
	}
}

static int lombo_pm_domain_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node;
	int domain_number;
	int error = -EINVAL;

	/* first: we should know how many domains should be added */
	error = of_property_read_u32(np, "number-domains", &domain_number);
	if (error < 0) {
		dev_err(dev, "failed to get domain number %s: %d\n",
			np->name, error);
		return error;
	}

	lpd = kcalloc(1, sizeof(struct lombo_pm_domain), GFP_KERNEL);
	if (lpd == NULL) {
		dev_err(dev, "failed to malloc space for pm domain: %s, %d\n",
			__func__, __LINE__);
		return -EINVAL;
	}

	lpd->r = kcalloc(domain_number, sizeof(struct regulator *), GFP_KERNEL);
	if (lpd->r == NULL) {
		dev_err(dev, "failed to malloc space for pm domain: %s, %d\n",
			__func__, __LINE__);
		goto err_0;
	}

	lpd->p = kcalloc(domain_number,
			sizeof(struct generic_pm_domain), GFP_KERNEL);
	if (lpd->p == NULL) {
		dev_err(dev, "failed to malloc space for pm domain: %s, %d\n",
			__func__, __LINE__);
		goto err_1;
	}

	error = lombo_pm_add_domains(lpd, np);
	if (error) {
		dev_err(dev, "failed to add pm domains:%s, %d\n", __func__,
			__LINE__);
		goto err_2;
	}

	return 0;

err_2:
	kfree(lpd->p);
err_1:
	kfree(lpd->r);
err_0:
	kfree(lpd);

	return -EINVAL;
}

static int lombo_pm_domain_remove(struct platform_device *pdev)
{
	lpd->dindex = 0;
	lombo_pm_remove_domains(lpd, pdev->dev.of_node);

	kfree(lpd->p);
	kfree(lpd->r);
	kfree(lpd);

	return 0;
}

static const struct of_device_id lombo_pm_domain_dt_match[] = {
	{ .compatible = "lombo,power-controller-ip6103" },
	{ .compatible = "lombo,power-controller-discrete" },
	{ /* sentinel */ },
};

static struct platform_driver lombo_pm_domain_driver = {
	.probe = lombo_pm_domain_probe,
	.remove = lombo_pm_domain_remove,
	.driver = {
		.name   = "lombo-pm-domain",
		.of_match_table = lombo_pm_domain_dt_match,
	},
};

static int __init lombo_pm_domain_drv_register(void)
{
	return platform_driver_register(&lombo_pm_domain_driver);
}
postcore_initcall_sync(lombo_pm_domain_drv_register);

static void __exit lombo_pm_domain_drv_unregister(void)
{
	platform_driver_unregister(&lombo_pm_domain_driver);
}
module_exit(lombo_pm_domain_drv_unregister);

static LIST_HEAD(pm_domain_link_head);

struct pm_domain_link_list {
	struct device **link_dev;
	struct device_link **link;
	struct device *dev;
	int count;
	struct list_head node;
};

static int pm_domain_notifier_call(struct notifier_block *nb,
				 unsigned long action, void *data)
{
	int error = 0, count = 0, i;
	struct device *dev = data;
	struct pm_domain_link_list *list;

	switch (action) {
	case BUS_NOTIFY_BIND_DRIVER:

/**
 *		error = pm_clk_create(dev);
 *		if (error) {
 *			pr_warning("%s:%d, pm clk create error\n", __func__, __LINE__);
 *			error = 0;
 *			break;
 *		}
 *
 *		error = of_pm_clk_add_clks(dev);
 *		if (error < 0) {
 *			pr_warning("%s:%d, pm clk add clks error:%d\n", __func__, __LINE__, error);
 *			pm_clk_destroy(dev);
 *			error = 0;
 *			break;
 *		}
 */
		count = of_count_phandle_with_args(dev->of_node, "power-domains",
			"#power-domain-cells");
		if (count <= 1) {
			pr_info("%s:%d, power domain not big than 1\n", __func__, __LINE__);
			break;
		}

		list = kmalloc(sizeof(struct pm_domain_link_list), GFP_KERNEL);
		if (list == NULL) {
			pr_err("%s:%d, malloc pm domain link list error\n", __func__, __LINE__);
			error = -ENOMEM;
			break;
		}

		list->link_dev = kmalloc(sizeof(struct device *) * count, GFP_KERNEL);
		if (list->link_dev == NULL) {
			kfree(list);
			pr_err("%s:%d, malloc pm domain link dev error\n", __func__, __LINE__);
			error = -ENOMEM;
			break;
		}

		list->link = kmalloc(sizeof(struct device_link *) * count, GFP_KERNEL);
		if (list->link == NULL) {
			kfree(list->link_dev);
			kfree(list);
			pr_err("%s:%d, malloc pm domain link error\n", __func__, __LINE__);
			error = -ENOMEM;
			break;
		}

		INIT_LIST_HEAD(&list->node);
		list->dev = dev;
		list->count = count;

		for (i = 0; i < count; ++i) {
			list->link_dev[i] = dev_pm_domain_attach_by_id(dev, i);
			if (list->link_dev[i] == NULL) {
				pr_err("attach pd[%d] failed: %s:%d\n",
					i, __func__, __LINE__);
				goto error_0;
			}

			list->link[i] = device_link_add(dev, list->link_dev[i],
				DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME
				| DL_FLAG_RPM_ACTIVE);
			if (list->link[i] == NULL) {
				pr_err("%s:%d\n", __func__, __LINE__);
				dev_pm_domain_detach(list->link_dev[i], true);
				goto error_0;
			}
		}

		list_add(&list->node, &pm_domain_link_head);

		break;

	case BUS_NOTIFY_UNBOUND_DRIVER:

		list_for_each_entry(list, &pm_domain_link_head, node) {
			if (list->dev == dev)
				break;
		}

		if (&list->node == &pm_domain_link_head)
			break;

		for (i = 0; i < list->count; ++i) {
			device_link_del(list->link[i]);
			dev_pm_domain_detach(list->link_dev[i], true);
		}

		kfree(list->link);
		kfree(list->link_dev);
		list_del_init(&list->node);
		kfree(list);
		break;
	}

	return error;

error_0:
	for (i -= 1; i >= 0; --i) {
		device_link_del(list->link[i]);
		dev_pm_domain_detach(list->link_dev[i], true);
	}

	kfree(list->link);
	kfree(list->link_dev);
	kfree(list);

	return -EINVAL;
}

static struct notifier_block pm_domain_notifier = {
	.notifier_call = pm_domain_notifier_call,
};

static int __init lombo_pm_domain_add_notifier(void)
{
	int ret;

	ret = bus_register_notifier(&platform_bus_type, &pm_domain_notifier);

	return ret;
}
core_initcall(lombo_pm_domain_add_notifier);
